<?php

 /**
  * @package JooS
  * @subpackage PHPackager
  */

  class JooS_PHPackager_FS {
    /**
     * @var JooS_PHPackager_FS
     */
    protected static $_instance = null;

    private static $_started = false;

    private $_root = null;

    protected $_entities = array();

    protected $_actions = array();

    /**
     * @param string $root
     * @return JooS_PHPackager_FS
     */
    public static function startTransaction($root) {
      if (self::inTransaction()) {
        require_once "JooS/PHPackager/FS/Exception.php";

        throw new JooS_PHPackager_FS_Exception("New FS-transaction could not be started. You must rollback or commit previous FS-transaction");
      }
      elseif (!is_string($root) || !is_dir($root)) {
        require_once "JooS/PHPackager/FS/Exception.php";

        throw new JooS_PHPackager_FS_Exception("Root directory is not directory");
      }

      self::$_started = true;
      self::clearTransactionData();
      
      self::getInstance()
        ->setRoot($root);
    }

    public static function rollbackTransaction() {
      if (self::inTransaction()) {
        self::clearTransactionData();
        self::$_started = false;
      }
    }

    public static function commitTransaction() {
      if (self::inTransaction()) {
        foreach (self::getInstance()->getActionsList() as $action) {
          /* @var $action JooS_PHPackager_FS_Action_Abstract */
          
          $action();
        }
        self::clearTransactionData();
        self::$_started = false;
      }
    }

    private static function clearTransactionData() {
      self::getInstance()
        ->setRoot(null)
        ->clearActionsList()
        ->clearEntitiesList();
    }

    /**
     * @return JooS_PHPackager_FS
     */
    public static function getInstance() {
      if (is_null(self::$_instance)) {
        self::$_instance = new self();
      }
      return self::$_instance;
    }

    public static function inTransaction() {
      return self::$_started;
    }

    /**
     * @param string $path
     * @param int $mode
     */
    public function mkdir($path, $mode = 0777) {
      if (!self::inTransaction()) {
        require_once "JooS/PHPackager/FS/Exception.php";

        throw new JooS_PHPackager_FS_Exception("Transaction must be started");
      }

      $entity = $this->getEntity($path);
      $realPath = $entity->path();

      if ($entity->file_exists()) {
        require_once "JooS/PHPackager/FS/Exception.php";
        
        throw new JooS_PHPackager_FS_Exception("Directory '$realPath' already exists");
      }
      else {
        // Проверка существования родительской директории. Строку не удалять !!11одинодин
        $this->_getParentEntity($entity, true);

        require_once "JooS/PHPackager/FS/Action/Mkdir.php";

        $action = JooS_PHPackager_FS_Action_Mkdir::newInstance();
        $action->realPath = $realPath;
        $action->mode = $mode;

        $this->addAction($action);
        $this->addEntity($realPath, JooS_PHPackager_FS_Entity::DIR);
      }
    }

    /**
     * @param string $path
     * @param int $mode
     */
    public function chmod($path, $mode) {
      if (!self::inTransaction()) {
        require_once "JooS/PHPackager/FS/Exception.php";

        throw new JooS_PHPackager_FS_Exception("Transaction must be started");
      }
      $entity = $this->getEntity($path);
      $realPath = $entity->path();

      if ($entity->is_readable() && $entity->is_writable()) {
        require_once "JooS/PHPackager/FS/Action/Chmod.php";

        $action = JooS_PHPackager_FS_Action_Chmod::newInstance();
        $action->realPath = $realPath;
        $action->mode = $mode;

        $this->addAction($action);
      }
      else {
        require_once "JooS/PHPackager/FS/Exception.php";

        throw new JooS_PHPackager_FS_Exception("File or Directory '$path' not found or must be writable");
      }
    }

    /**
     * @param string $path
     * @param JooS_PHPackager_File_Adapter|string $contents
     * @return JooS_PHPackager_File_Adapter
     */
    public function file_put_contents($path, $contents) {
      if (!self::inTransaction()) {
        require_once "JooS/PHPackager/FS/Exception.php";

        throw new JooS_PHPackager_FS_Exception("Transaction must be started");
      }
      
      $entity = $this->getEntity($path);
      $realPath = $entity->path();

      if ($entity->file_exists()) {
        require_once "JooS/PHPackager/FS/Exception.php";
        
        throw new JooS_PHPackager_FS_Exception("File '$realPath' already exists");
      }
      else {
        $parentDir = $this->_getParentEntity($entity, true);

        require_once "JooS/PHPackager/File.php";
        $tmpFile = JooS_PHPackager_File::createAdapter("Tmp");

        if ($contents instanceof JooS_PHPackager_File_Adapter) {
          $contents->copy($tmpFile);
        }
        else {
          $tmpFile->file_put_contents($contents);
        }

        require_once "JooS/PHPackager/FS/Action/CopyTmp.php";

        $action = JooS_PHPackager_FS_Action_CopyTmp::newInstance();
        $action->tmpFile = $tmpFile;
        $action->realPath = $realPath;

        $this->addAction($action);
        $this->addEntity($realPath, JooS_PHPackager_FS_Entity::FILE);

        return $tmpFile;
      }
    }

    /**
     * @param string $source
     * @param string $path
     */
    public function copy($source, $path) {
      if (!self::inTransaction()) {
        require_once "JooS/PHPackager/FS/Exception.php";

        throw new JooS_PHPackager_FS_Exception("Transaction must be started");
      }
      $realPath = $this->getNormalizedPath(realpath($source));
      if (is_file($realPath)) {
        require_once "JooS/PHPackager/File.php";
        require_once "JooS/Event/Fs/File/Create.php";

        $sourceFile = JooS_PHPackager_File::createAdapter("File")->set_name($realPath);
        $tmpFile = $this->file_put_contents($path, $sourceFile);
        
        JooS_Event_Fs_File_Create::getInstance()
          ->setRealPath($realPath)
          ->setPath($path)
          ->setTmpFile($tmpFile)
          ->notify();
      }
      elseif (is_dir($realPath)) {
        $objects = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($realPath), RecursiveIteratorIterator::SELF_FIRST);
        foreach ($objects as $name => $splFileInfo) {
          /* @var $splFileInfo SplFileInfo */
          $realName = $this->getNormalizedPath($name);
          $relativeName = $path . "/" . $this->_getRelativePath($realName, $realPath);

          if ($splFileInfo->isFile()) {
            $this->copy($realName, $relativeName);
          }
          elseif (!$splFileInfo->isDir()) {
            require_once "JooS/PHPackager/FS/Exception.php";
            
            throw new JooS_PHPackager_FS_Exception("'$realName' must be a File or Directory");
          }
        }
      }
      else {
        require_once "JooS/PHPackager/FS/Exception.php";
        
        throw new JooS_PHPackager_FS_Exception("'$source' must be File or Directory, or not found ");
      }
    }

    /**
     * @param string $source
     * @param bool $cleanupDirectories
     */
    public function delete($source, $cleanupDirectories = false) {
      if (!self::inTransaction()) {
        require_once "JooS/PHPackager/FS/Exception.php";

        throw new JooS_PHPackager_FS_Exception("Transaction must be started");
      }
      $realPath = $this->getNormalizedPath($source);

      $path = $this->_getRelativePath($realPath);
      $entity = $this->getEntity($path);

      if ($entity->is_readable() && $entity->is_writable()) {
        if ($entity->is_file()) {
          require_once "JooS/PHPackager/FS/Action/Unlink.php";

          $action = JooS_PHPackager_FS_Action_Unlink::newInstance();
          $action->realPath = $realPath;
          $action->cleanupDirectories = $cleanupDirectories;

          $this->addAction($action);
          $this->addEntity($realPath, JooS_PHPackager_FS_Entity::NONE);

          require_once "JooS/Event/Fs/File/Delete.php";
          
          JooS_Event_Fs_File_Delete::getInstance()
            ->setRealPath($realPath)
            ->notify();
        }
        elseif ($entity->is_dir()) {
          foreach ($this->getEntitiesList() as $_path => $_entity) {
            /* @var $_entity JooS_PHPackager_FS_Entity */
            if ($_path != $realPath && substr($_path, 0, strlen($realPath)) == $realPath) {
              if (!($_entity instanceof JooS_PHPackager_FS_Entity_None)) {
                return;
              }
            }
          }

          if (file_exists($realPath)) {
            $files = glob(rtrim($realPath, "/") . "/" . "*");
            foreach ($files as $_path) {
              $_path = $this->getNormalizedPath($_path);
              if (!$this->issetEntity($_path)) {
                return;
              }
            }
          }

          require_once "JooS/PHPackager/FS/Action/Rmdir.php";

          $action = JooS_PHPackager_FS_Action_Rmdir::newInstance();
          $action->realPath = $realPath;

          $this->addAction($action);
          $this->addEntity($realPath, JooS_PHPackager_FS_Entity::NONE);
        }
      }
    }

    /**
     * @param string $root
     */
    protected function __construct() {
    }

    /**
     * @return string
     */
    protected function getRoot() {
      return $this->_root;
    }

    /**
     * @param string $root
     * @return JooS_PHPackager_FS
     */
    protected function setRoot($root) {
      $this->_root = $this->getNormalizedPath($root);
      
      return $this;
    }

    /**
     * @param JooS_PHPackager_FS_Action_Interface $action
     * @return JooS_PHPackager_FS
     */
    protected function addAction(JooS_PHPackager_FS_Action_Interface $action) {
      $this->_actions[] = $action;

      return $this;
    }

    /**
     * @return JooS_PHPackager_FS
     */
    protected function clearActionsList() {
      $this->_actions = array();

      return $this;
    }

    /**
     * @return array
     */
    protected function getActionsList() {
      return $this->_actions;
    }
    
    /**
     * @param string $realPath
     * @param string $type
     * @return JooS_PHPackager_FS 
     */
    protected function addEntity($realPath, $type) {
      $this->_entities[$realPath] = JooS_PHPackager_FS_Entity::getInstance($realPath, $type);

      return $this;
    }

    /**
     * @param string $path
     * @return bool
     */
    protected function issetEntity($realPath) {
      return isset($this->_entities[$realPath]);
    }

    /**
     * @param string $path
     * @return JooS_PHPackager_FS_Entity
     */
    protected function getEntity($path) {
      $realPath = $this->_getRealPath($path);

      require_once "JooS/PHPackager/FS/Entity.php";

      if (!$this->issetEntity($realPath)) {
        $this->_entities[$realPath] = JooS_PHPackager_FS_Entity::getInstance($realPath);
      }
      return $this->_entities[$realPath];
    }

    /**
     * @return JooS_PHPackager_FS
     */
    protected function clearEntitiesList() {
      $this->_entities = array();
      
      return $this;
    }

    /**
     * @return array
     */
    protected function getEntitiesList() {
      return $this->_entities;
    }

    /**
     *
     * @param JooS_Stream_Entity_Interface $entity
     * @param bool $check
     * @return JooS_PHPackager_FS_Entity
     */
    protected function _getParentEntity(JooS_Stream_Entity_Interface $entity, $check = false) {
      $dirname = $entity->dirname();
      $entity = $this->getEntity( $this->_getRelativePath($dirname) );
      if ($check) {
        $this->_checkParentEntity($entity);
      }
      return $entity;
    }

    /**
     * @param JooS_Stream_Entity_Interface $parentDir
     */
    protected function _checkParentEntity(JooS_Stream_Entity_Interface &$parentDir) {
      if (!$parentDir->file_exists()) {
        $path = $this->_getRelativePath($parentDir);
        $this->mkdir($path);
        $parentDir = $this->getEntity($path);
      }
      elseif (!$parentDir->is_dir()) {
        require_once "JooS/PHPackager/FS/Exception.php";
        
        throw new JooS_PHPackager_FS_Exception("File '" . $parentDir->path(). "' must be Directory");
      }
      if (!$parentDir->is_writable()) {
        require_once "JooS/PHPackager/FS/Exception.php";
        
        throw new JooS_PHPackager_FS_Exception("Parent directory '" . $parentDir->path(). "' must be writable");
      }
    }

    /**
     * @param string $path
     * @return string
     */
    public function getNormalizedPath($path) {
      return str_replace("\\", "/", $path);
    }
    
    /**
     * @param string $path
     * @return string
     */
    protected function _getRealPath($path) {
      $root = $this->getRoot();

      $path = $this->getNormalizedPath($path);
      $path = $root . "/" . trim($path, "/");

      while (strpos($path, "//") !== false) {
        $path = str_replace("//", "/", $path);
      }
      return $path;
    }

    /**
     * @param string $realPath
     * @param string $root
     * @return string
     */
    protected function _getRelativePath($realPath, $root = null) {
      if (is_object($realPath) && $realPath instanceof JooS_Stream_Entity_Interface) {
        /* @var JooS_Stream_Entity_Interface $realPath */
        $realPath = $realPath->path();
      }

      if (is_null($root)) {
        $root = $this->getRoot();
      }
      $pos = strpos($realPath, $root);

      if ($pos === 0) {
        return substr($realPath, strlen($root) + 1);
      }
      else {
        require_once "JooS/PHPackager/FS/Exception.php";

        throw new JooS_PHPackager_FS_Exception("Root path '" . $root . "' must contain real path '$realPath'");
      }
    }
  }
